package com.zlyx.easy.api.extension;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.validation.constraints.NotNull;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.zlyx.easy.api.annotations.ApiDesc;
import com.zlyx.easy.api.context.ApiContext;
import com.zlyx.easy.api.model.ApiModule;
import com.zlyx.easy.api.property.ListProperty;
import com.zlyx.easy.core.buffer.EasyBuffer;
import com.zlyx.easy.core.map.Maps;
import com.zlyx.easy.core.tool.BaseType;
import com.zlyx.easy.core.utils.ClassUtils;
import com.zlyx.easy.core.utils.MethodUtils;
import com.zlyx.easy.core.utils.StringUtils;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.ResponseHeader;
import io.swagger.converter.ModelConverters;
import io.swagger.models.HttpMethod;
import io.swagger.models.Model;
import io.swagger.models.Response;
import io.swagger.models.Scheme;
import io.swagger.models.Swagger;
import io.swagger.models.Tag;
import io.swagger.models.parameters.Parameter;
import io.swagger.models.parameters.QueryParameter;
import io.swagger.models.properties.ArrayProperty;
import io.swagger.models.properties.MapProperty;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.RefProperty;
import io.swagger.util.BaseReaderUtils;
import io.swagger.util.ParameterProcessor;
import io.swagger.util.PathUtils;
import io.swagger.util.ReflectionUtils;

@SuppressWarnings("deprecation")
public class ApiReaderExtension implements ReaderExtension {

	@Override
	public boolean isReadable(ApiContext context) {
		Api api = ReflectionUtils.getAnnotation(context.getCls(), Api.class);
		return api != null && !api.hidden();
	}

	@Override
	public void applyConsumes(ApiContext context, ApiModule module, Method method) {
		final Set<String> consumes = new HashSet<>();
		final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);

		if (apiOperation != null) {
			consumes.addAll(parseStringValues(apiOperation.consumes()));
		}

		if (consumes.isEmpty()) {
			final Api apiAnnotation = ReflectionUtils.getAnnotation(context.getCls(), Api.class);
			if (apiAnnotation != null) {
				consumes.addAll(parseStringValues(apiAnnotation.consumes()));
			}
			consumes.addAll(context.getParentConsumes());
		}
		for (String consume : consumes) {
			module.consumes(consume);
		}
	}

	public static List<String> parseStringValues(String str) {
		return parseAnnotationValues(str, new Function<String, String>() {
			@Override
			public String apply(String value) {
				return value;
			}
		});
	}

	public static <T> List<T> parseAnnotationValues(String str, Function<String, T> processor) {
		final List<T> result = new ArrayList<T>();
		for (String item : Splitter.on(",").trimResults().omitEmptyStrings().split(str)) {
			result.add(processor.apply(item));
		}
		return result;
	}

	@Override
	public void applyProduces(ApiContext context, ApiModule module, Method method) {
		final List<String> produces = new ArrayList<String>();
		final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);

		if (apiOperation != null) {
			produces.addAll(parseStringValues(apiOperation.produces()));
		}

		if (produces.isEmpty()) {
			final Api apiAnnotation = ReflectionUtils.getAnnotation(context.getCls(), Api.class);
			if (apiAnnotation != null) {
				produces.addAll(parseStringValues(apiAnnotation.produces()));
			}
			produces.addAll(context.getParentProduces());
		}
		for (String produce : produces) {
			module.produces(produce);
		}
	}

	@Override
	public String getHttpMethod(ApiContext context, Method method) {
		final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
		return apiOperation == null || StringUtils.isEmpty(apiOperation.httpMethod()) ? HttpMethod.POST.name()
				: apiOperation.httpMethod();
	}

	@Override
	public String getPath(ApiContext context, ApiModule module, Method method) {
		final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
		String operationId = null == apiOperation ? ""
				: StringUtils.isEmpty(apiOperation.nickname()) ? null : apiOperation.nickname();
		return PathUtils.collectPath(context.getParentPath(), context.getInterfaceCls().getName(), method.getName(),
				operationId);
	}

	@Override
	public String applyTags(ApiContext context, ApiModule module, Method method) {
		String tag = module.getTag();
		Api apiAnnotation = ReflectionUtils.getAnnotation(context.getCls(), Api.class);
		if (apiAnnotation != null && StringUtils.isNotEmpty(apiAnnotation.value())) {
			tag = apiAnnotation.value();
		}
		ApiDesc apiDesc = ReflectionUtils.getAnnotation(context.getCls(), ApiDesc.class);
		if (apiDesc != null && StringUtils.isNotEmpty(apiDesc.value())) {
			tag = apiDesc.value();
		}
		if (StringUtils.isEmpty(tag)) {
			tag = context.getInterfaceCls().getSimpleName();
		}
		module.tag(tag);
		context.getSwagger().addTag(new Tag().name(tag));
		return tag;
	}

	@Override
	public void applyOperationId(ApiModule module, Method method) {
		module.operationId(method.getName());
	}

	@Override
	public void applySummary(ApiModule module, Method method) {
		String summary = module.getSummary();
		ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
		if (apiOperation != null && StringUtils.isNotEmpty(apiOperation.value())) {
			summary = apiOperation.value();
		}
		ApiDesc apiDesc = ReflectionUtils.getAnnotation(method, ApiDesc.class);
		if (apiDesc != null && StringUtils.isNotEmpty(apiDesc.value())) {
			summary = apiDesc.value();
		}
		if (StringUtils.isEmpty(summary)) {
			summary = method.getName();
		}
		module.summary(summary);
	}

	@Override
	public void applyDescription(ApiModule module, Method method) {
		String description = module.getDescription();
		ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
		if (apiOperation != null) {
			if (StringUtils.isNotEmpty(apiOperation.notes())) {
				description = apiOperation.notes();
			} else if (StringUtils.isNotEmpty(apiOperation.value())) {
				description = apiOperation.value();
			}
		}
		ApiDesc apiDesc = ReflectionUtils.getAnnotation(method, ApiDesc.class);
		if (apiDesc != null && StringUtils.isNotEmpty(apiDesc.notes())) {
			description = apiDesc.notes();
		}
		if (StringUtils.isEmpty(description)) {
			description = method.getName();
		}
		module.description(description);
	}

	@Override
	public void applySchemes(ApiContext context, ApiModule module, Method method) {
		final List<Scheme> schemes = new ArrayList<Scheme>();
		final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
		final Api apiAnnotation = ReflectionUtils.getAnnotation(context.getCls(), Api.class);

		if (apiOperation != null) {
			schemes.addAll(parseSchemes(apiOperation.protocols()));
		}

		if (schemes.isEmpty() && apiAnnotation != null) {
			schemes.addAll(parseSchemes(apiAnnotation.protocols()));
		}

		for (Scheme scheme : schemes) {
			module.scheme(scheme);
		}
	}

	public static List<Scheme> parseSchemes(String schemes) {
		final List<Scheme> result = new ArrayList<Scheme>();
		for (String item : schemes.split(",")) {
			final Scheme scheme = Scheme.forValue(item);
			if (scheme != null && !result.contains(scheme)) {
				result.add(scheme);
			}
		}
		return result;
	}

	@Override
	public void setDeprecated(ApiModule module, Method method) {
		module.deprecated(ReflectionUtils.getAnnotation(method, Deprecated.class) != null);
	}

	@Override
	public void applySecurityRequirements(ApiContext context, ApiModule module, Method method) {
	}

	public static final String SUCCESSFUL_OPERATION = "";

	public static boolean isValidResponse(Type type) {
		final JavaType javaType = TypeFactory.defaultInstance().constructType(type);
		return !ReflectionUtils.isVoid(javaType);
	}

	public static Map<String, Property> parseResponseHeaders(ApiContext context, ResponseHeader[] headers) {
		Map<String, Property> responseHeaders = null;
		for (ResponseHeader header : headers) {
			final String name = header.name();
			if (StringUtils.isNotEmpty(name)) {
				if (responseHeaders == null) {
					responseHeaders = Maps.newMap();
				}
				final Class<?> cls = header.response();
				if (!ReflectionUtils.isVoid(cls)) {
					final Property property = ModelConverters.getInstance().readAsProperty(cls);
					if (property != null) {
						final Property responseProperty = ContainerWrapper.wrapContainer(header.responseContainer(),
								property, ContainerWrapper.ARRAY, ContainerWrapper.LIST, ContainerWrapper.SET);
						responseProperty.setDescription(header.description());
						responseHeaders.put(name, responseProperty);
						appendModels(context.getSwagger(), cls);
					}
				}
			}
		}
		return responseHeaders;
	}

	public static void appendModels(Swagger swagger, Type type) {
		final Map<String, Model> models = ModelConverters.getInstance().readAll(type);
		for (Map.Entry<String, Model> entry : models.entrySet()) {
			swagger.model(entry.getKey(), entry.getValue());
		}
	}

	public static Type getResponseType(Method method) {
		final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
		if (apiOperation != null && !ReflectionUtils.isVoid(apiOperation.response())) {
			return apiOperation.response();
		} else {
			return method.getGenericReturnType();
		}
	}

	public static String getResponseContainer(ApiOperation apiOperation) {
		return apiOperation == null ? null : apiOperation.responseContainer();
	}

	@Override
	public void applyResponses(ApiContext context, ApiModule module, Method method) {
		final Map<Integer, Response> result = Maps.newMap();

		final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
		if (apiOperation != null && StringUtils.isNotEmpty(apiOperation.responseReference())) {
			final Response response = new Response().description(SUCCESSFUL_OPERATION);
			response.schema(new RefProperty(apiOperation.responseReference()));
			result.put(apiOperation.code(), response);
		}
		final Type responseType = getResponseType(method);
		if (isValidResponse(responseType)) {
			final Property property = ModelConverters.getInstance().readAsProperty(responseType);
			if (property != null) {
				String container = getResponseContainer(apiOperation);
				if (StringUtils.isEmpty(container)) {
					container = method.getReturnType().getSimpleName().toLowerCase();
				}
				final Property responseProperty = ContainerWrapper.wrapContainer(container, property);
				final int responseCode = apiOperation == null ? 200 : apiOperation.code();
				final Map<String, Property> defaultResponseHeaders = apiOperation == null
						? Collections.<String, Property>emptyMap()
						: parseResponseHeaders(context, apiOperation.responseHeaders());
				final Response response = new Response().description(SUCCESSFUL_OPERATION).schema(responseProperty)
						.headers(defaultResponseHeaders);
				result.put(responseCode, response);
				appendModels(context.getSwagger(), responseType);
			}
		}

		final ApiResponses responseAnnotation = ReflectionUtils.getAnnotation(method, ApiResponses.class);
		if (responseAnnotation != null) {
			for (ApiResponse apiResponse : responseAnnotation.value()) {
				final Map<String, Property> responseHeaders = parseResponseHeaders(context,
						apiResponse.responseHeaders());

				final Response response = new Response().description(apiResponse.message()).headers(responseHeaders);

				if (StringUtils.isNotEmpty(apiResponse.reference())) {
					response.schema(new RefProperty(apiResponse.reference()));
				} else if (!ReflectionUtils.isVoid(apiResponse.response())) {
					final Type type = apiResponse.response();
					final Property property = ModelConverters.getInstance().readAsProperty(type);
					if (property != null) {
						response.schema(ContainerWrapper.wrapContainer(apiResponse.responseContainer(), property));
						appendModels(context.getSwagger(), type);
					}
				}
				result.put(apiResponse.code(), response);
			}
		}

		for (Map.Entry<Integer, Response> responseEntry : result.entrySet()) {
			if (responseEntry.getKey() == 0) {
				module.defaultResponse(responseEntry.getValue());
			} else {
				module.response(responseEntry.getKey(), responseEntry.getValue());
			}
		}
	}

	@Override
	public void applyParameters(ApiContext context, ApiModule module, Type type, Annotation[] annotations) {
	}

	@Override
	public void applyParameters(ApiContext context, ApiModule module, Method method) {
		Class<?>[] parameterTypes = method.getParameterTypes();
		java.lang.reflect.Parameter[] parameter = method.getParameters();
		String[] names = MethodUtils.getParameterNames(method);
		String description = null;
		String name = null;
		boolean required = false;
		ApiParam apiParam = null;
		List<ApiParam> apiParams = getParameterAnnotations(ApiParam.class, method);
		for (int i = 0; i < parameter.length; i++) {
			if (names != null && names.length >= i) {
				name = names[i];
			} else {
				name = parameter[i].getName();
			}
			description = name;
			apiParam = apiParams.get(i);
			if (apiParam != null) {
				if (!"".equals(apiParam.name())) {
					name = apiParam.name();
				}
				if (!"".equals(apiParam.value())) {
					description = apiParam.value();
				}
				required = apiParam.required();
			}
			if (parameterTypes[i].getAnnotation(NotNull.class) != null) {
				required = true;
			}
			applyParametersV2(context, module, name, description, required, parameterTypes[i], null, null);
		}
	}

	@SuppressWarnings("unchecked")
	public static <T> List<T> getParameterAnnotations(Class<T> clsAnnotation, Method method) {
		List<T> annotations = new ArrayList<>();
		Annotation[][] methodAnnotations = method.getParameterAnnotations();
		Method overriddenmethod = ReflectionUtils.getOverriddenMethod(method);
		if (overriddenmethod == null) {
			for (int i = 0; i < methodAnnotations.length; i++) {
				boolean flag = true;
				for (int j = 0; j < methodAnnotations[i].length; j++) {
					if (methodAnnotations[i][j].annotationType() == clsAnnotation) {
						annotations.add((T) methodAnnotations[i][j]);
						flag = false;
						break;
					}
				}
				if (flag) {
					annotations.add(null);
				}
			}
		} else {
			Annotation[][] overriddenAnnotations = overriddenmethod.getParameterAnnotations();
			for (int i = 0; i < methodAnnotations.length; i++) {
				boolean flag = true;
				for (int j = 0; j < methodAnnotations[i].length; j++) {
					if (methodAnnotations[i][j].annotationType() == clsAnnotation) {
						annotations.add((T) methodAnnotations[i][j]);
						flag = false;
					}
				}
				if (flag && overriddenmethod != null) {
					for (int j = 0; j < overriddenAnnotations[i].length; j++) {
						if (overriddenAnnotations[i][j].annotationType() == clsAnnotation) {
							annotations.add((T) overriddenAnnotations[i][j]);
							flag = false;
						}
					}
					if (flag) {
						annotations.add(null);
					}
				}
			}
		}
		return annotations;
	}

	public void applyParametersV2(ApiContext context, ApiModule module, String name, String description,
			boolean required, Class<?> cls, Class<?> pCls, String pName) {
		if (cls.getName().contains("java") || cls.getName().contains("sun")
				|| ClassUtils.isAssignableFrom(cls, Map.class) || ClassUtils.isAssignableFrom(cls, List.class)
				|| BaseType.isJavaType(cls)) {
			final QueryParameter parameter = new QueryParameter();
			if (StringUtils.isNotEmpty(module.getParam(name))) {
				description = module.getParam(name);
			} else if (pName != null) {
				description = EasyBuffer.newString("类型为", pCls.getSimpleName(), "的参数", pName, "的内部属性：", name);
				name = pName + "：" + name;
			} else if (StringUtils.isEmpty(description)) {
				description = name;
			}
			parameter.setName(name);
			parameter.setDescription(description);
			if ("T".equals(cls.getTypeName())) {
				parameter.setType("object");
			} else {
				parameter.setType(cls.getSimpleName());
			}
			parameter.setRequired(required);
			parameter.setAccess("true");
			module.parameter(parameter);
		} else {
			Field[] fields = cls.getDeclaredFields();
			ApiModelProperty apiModelProperty = null;
			String fdescription = null;
			for (Field field : fields) {
				if (!Modifier.isStatic(field.getModifiers()) && !Modifier.isFinal(field.getModifiers())) {
					apiModelProperty = field.getAnnotation(ApiModelProperty.class);
					fdescription = apiModelProperty != null ? apiModelProperty.value() : field.getName();
					required = apiModelProperty != null ? apiModelProperty.required() : required;
					applyParametersV2(context, module, field.getName(), fdescription, required, field.getType(), cls,
							name);
				}
			}
		}
	}

	@Override
	public void applyImplicitParameters(ApiContext context, ApiModule module, Method method) {
		final ApiImplicitParams implicitParams = ReflectionUtils.getAnnotation(method, ApiImplicitParams.class);
		if (implicitParams != null && implicitParams.value().length > 0) {
			Map<String, java.lang.reflect.Parameter> parameters = MethodUtils.getParamterMap(method);
			for (ApiImplicitParam param : implicitParams.value()) {
				final Parameter p = readImplicitParam(context.getSwagger(), parameters, param);
				if (p != null) {
					module.parameter(p);
				}
			}
		}
	}

	public Parameter readImplicitParam(Swagger swagger, Map<String, java.lang.reflect.Parameter> parameters,
			ApiImplicitParam param) {
		final Parameter p = new QueryParameter();
		Type type = null;
		if (!"".equals(param.name()) && !"".equals(param.dataType())) {
			if (param.dataType().toLowerCase().contains("list")) {
				type = List.class;
			} else if (param.dataType().toLowerCase().contains("map")) {
				type = Map.class;
			} else {
				type = ReflectionUtils.typeFromString(param.dataType());
			}
		}
		if (type == null && parameters.containsKey(param.name())) {
			type = parameters.get(param.name()).getType();
		}
		return ParameterProcessor.applyAnnotations(swagger, p, type == null ? String.class : type,
				Collections.<Annotation>singletonList(param));
	}

	@Override
	public void applyExtensions(ApiContext context, ApiModule module, Method method) {
		final ApiOperation apiOperation = ReflectionUtils.getAnnotation(method, ApiOperation.class);
		if (apiOperation != null) {
			module.getVendorExtensions().putAll(BaseReaderUtils.parseExtensions(apiOperation.extensions()));
		}
	}

	enum ContainerWrapper {
		LIST("list") {
			@Override
			protected Property doWrap(Property property) {
				return new ListProperty(property);
			}
		},
		ARRAY("array") {
			@Override
			protected Property doWrap(Property property) {
				return new ArrayProperty(property);
			}
		},
		MAP("map") {
			@Override
			protected Property doWrap(Property property) {
				return new MapProperty(property);
			}
		},
		SET("set") {
			@Override
			protected Property doWrap(Property property) {
				ArrayProperty arrayProperty = new ArrayProperty(property);
				arrayProperty.setUniqueItems(true);
				return arrayProperty;
			}
		};

		private final String container;

		ContainerWrapper(String container) {
			this.container = container;
		}

		public static Property wrapContainer(String container, Property property, ContainerWrapper... allowed) {
			final Set<ContainerWrapper> tmp = allowed.length > 0 ? EnumSet.copyOf(Arrays.asList(allowed))
					: EnumSet.allOf(ContainerWrapper.class);
			for (ContainerWrapper wrapper : tmp) {
				final Property prop = wrapper.wrap(container, property);
				if (prop != null) {
					return prop;
				}
			}
			return property;
		}

		public Property wrap(String container, Property property) {
			if (this.container.equalsIgnoreCase(container)) {
				return doWrap(property);
			}
			return null;
		}

		protected abstract Property doWrap(Property property);
	}

}
